﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Net;
using System.Text;
using System.Threading.Tasks;

namespace Zhy.Communication
{
    public class SocketCommunication
    {
        Socket client;

        public async void SocketTest()
        {
            //StartTcpServer(); // TCP 服务端

            //StartTcpClient(); // TCP客户端

            //StartUdp();  // UDP代码测试

            //TcpTest(); // 粘包和丢包问题测试

            //TcpReconnect(); // 解决断线重连问题

            //TcpClientTest(); // TCPClient代码测试

            UdpClientTest(); // UdpClient代码测试
        }

        #region 服务端代码
        /// <summary>
        /// Soceket 服务端代码
        /// </summary>
        private void StartTcpServer()
        {
            Socket server;
            // AddressFamily addressFamily（InterNetwork-IPv4协议）, - 地址协议簇：IPv4   IPv6
            //SocketType socketType, - 数据传输类型：TCP协议-数据流方式(Stream)    UDP协议-数据报(Dgram)
            //ProtocolType protocolType  - 协议类型
            //socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            // 实例化socket对象
            server = new Socket(SocketType.Stream, ProtocolType.Tcp); // 默认使用的是IPV4

            // TCP通信之前：操作---确认网络是通的   
            //   cmd窗口    ping 127.0.0.1   服务端不一定需要能发现客户端，但是客户端必须要能发现服务端
            //             telnet 127.0.0.1 9090
            // 区分  服务端  --  客户端
            #region 服务端-- 打开一个监听   本机IP  指定端口号
            // 这里是给Socket对象指定特定的IP和端口
            server.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9090));
            // 可以无参，也可以有参：不是连接数- 10表示排队将要连接的数量
            //server.Listen(10); // 监听  10表示允许连接排队的数量
            server.Listen(); // 监听 默认可以不设置排队数量

            // 允许客户端接入的请求，返回的对象是客户端的连接
            // 有客户端的连接对象，可以进行指定客户端的数据发送

            #region 同步接收连接和数据 - 不推荐
            // 同步接收连接和数据
            //Task.Run(async () =>
            //{
            // 同步执行    卡线程
            // var sss = server.Accept();  // 卡线程
            //});
            #endregion

            #region 异步接收连接和数据
            // 异步执行，第一种异步方式  - 推荐
            // 异步的允许客户端接入的请求，返回的对象是客户端的连接
            var result = server.BeginAccept(new AsyncCallback(OnAcceptCallback), server);

            // 异步执行，第二种异步方式
            //var sss = await server.AcceptAsync();

            // 异步执行，第三种异步方式
            //CancellationTokenSource token = new CancellationTokenSource();
            //server.AcceptAsync(token.Token);
            //token.Cancel();
            #endregion
            #endregion
        }

        /// <summary>
        /// 服务端对于客户端接入请求的响应事件
        /// 使用回调的方式持续接收连接和处理数据
        /// </summary>
        /// <param name="result"></param>
        private async void OnAcceptCallback(IAsyncResult result)
        {
            #region 获取服务端与客户端的连接对象
            // 这里有客户端接入
            // 
            var server = result.AsyncState as Socket; // 获取服务端的连接对象
            var client = server.EndAccept(result); // 获取客户端连接的对象
            #endregion

            #region 接收客户端发送过来的数据
            /* 这接收的数据长度可以通过协议，分段获取
             * 比如接收一个固定长度的头部，然后获取整个报文的长度，
             * 然后再跟接收到的长度设置后续接收的数组长度，
             * 通过这种方式来动态设置接收数据长度。
             */
            // 声明一个10M大小的空间用于接收数据
            byte[] data = new byte[1024 * 1024 * 10];// 通信协议里   会有长度限制    TCP头   固定长度    （包含数据长）

            // 同步接收，对当前线程是有影响   卡
            //Task.Run(() =>
            //{
            // 同步接收数据
            //int count = client.Receive(data);
            //});

            // 异步接收数据
            // 如果count 接收到的是0有可能代表客户端离线连接断开了
            int count = await client.ReceiveAsync(data); // 返回接收到的字节数
            #endregion

            #region 服务端向客户端发送数据
            // 同步向客户端发送数据
            //client.Send(new byte[] { 0x20, 0x30 });
            // 异步向客户端发送数据
            await client.SendAsync(new byte[] { 0x20, 0x30 });
            #endregion


            #region 打印连接地址
            Console.WriteLine(client.LocalEndPoint); //  打印本地服务端地址
            Console.WriteLine(client.RemoteEndPoint); // 打印客户端地址
            #endregion

            // 回调执行下一次的客户端接入请求
            server.BeginAccept(new AsyncCallback(OnAcceptCallback), server); // 回调接收连接和数据
        }
        #endregion

        #region 客户端代码
        /// <summary>
        /// 客户端代码
        /// </summary>
        private async void StartTcpClient()
        {
            #region 客户端   指定一个IP和端口号  进行连接请求
            Socket client = new Socket(SocketType.Stream, ProtocolType.Tcp);
            // 接收超时时间  5秒
            //client.ReceiveTimeout = 5000; // 设置连接超时时间
            //client.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9090));
            await client.ConnectAsync(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9090)); // 客户端异步连接
            #endregion

            #region 客户端向服务端发送数据
            // 同步发送
            //int count = client.Send(new byte[] { 0x10, 0x11 });
            // 异步发送
            int count = await client.SendAsync(new byte[] { 0x10, 0x11 });
            #endregion

            #region 客户端接收服务端发送过来的数据
            byte[] data = new byte[1024 * 1024]; // 1024 * 1024 是1M的空间
            try
            {
                /// 1、连接正常，任意方下线，Receive方法会正常执行，但是返回值为0
                /// 2、连接任意方出现发送异常，对方的Receive方法会有异常
                count = await client.ReceiveAsync(data);
            }
            catch (Exception ex)
            {

            }
            #endregion

            // Socket对象的连接状态，是否是连接状态
            // 连接状态不准确，这里是指Socket对象的最后一次操作的状态
            // 需要？心跳  数据交换
            //client.Connected
        }
        #endregion

        #region Udp通讯操作
        /// <summary>
        /// 无连接的数据报的通信模式   -- 发送一次  一组数据    
        /// 数据不保证交付
        /// UDP是将数据打包，然后采用广播的形式发送数据，根据IP和端口号是谁的数据谁拿走，数据发送完就不管了，不保证交付的。
        /// </summary>
        private async void StartUdp()
        {
            // 实例化UDP通讯对象
            Socket client = new Socket(SocketType.Dgram, ProtocolType.Udp);
            // 发送/接收

            #region UDP指定终端发送数据
            //string str = "Hello";
            //var data = Encoding.ASCII.GetBytes(str);  // 非中文的
            //var dataChinese = Encoding.ASCII.GetBytes(str);  // 中文的
            //await client.SendToAsync(data, new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9090));
            // 发送   指定终端
            // 参数1 要发送的数据 参数2 指定要接收的终端
            //await client.SendToAsync(new byte[] { 0x40, 0x50 }, new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9090));
            // 同步发送
            //client.SendTo(new byte[] { 0x40, 0x50 }, new IPEndPoint(IPAddress.Parse("192.168.1.10"), 9090));
            #endregion

            #region UDP的数据群发方式
            // 发送   广播，局域网中所有终端都能收到
            // 设置UDP的群发模式 - 不设置的话无法进行群发送

            // 指定1-255的终端区域群发
            //client.SendTo(new byte[] { 0x40, 0x50 }, new IPEndPoint(IPAddress.Parse("192.168.43.255"), 9090)); // 这里只能使用固定IP
            // 不指定终端区域，所有可以接收的终端群发
            //client.SendTo(new byte[] { 0x40, 0x50 }, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 9090));
            #endregion

            #region 接收发送过来的数据
            // 接收
            client.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9999)); // 指定当前服务端端的接受地址
            byte[] bytes = new byte[1024 * 1024]; // 要接受的数据
            // 设置任意客户端地址-IPAddress.Any=0.0.0.0
            EndPoint ep = new IPEndPoint(IPAddress.Any, 0); // 设置客户端地址和端口

            // 异步接收发送过来的数据
            // 参数1 接收数据 参数2 接收的地址
            var result = await client.ReceiveFromAsync(bytes, ep);
            Console.WriteLine(result.RemoteEndPoint); // 接收到的地址
            Console.WriteLine(result.ReceivedBytes); // 接收到的字节数

            // 同步接收发送过来的数据
            //int count = client.ReceiveFrom(bytes, ref ep);
            //Console.WriteLine((ep as IPEndPoint).Address);
            #endregion

            #region 作为服务端给客户端回复消息
            // 异步回复方法
            await client.SendToAsync(new byte[] { 0x40, 0x50 }, result.RemoteEndPoint);
            // 同步回复方法
            //client.SendTo(new byte[] { 0x40, 0x50 }, ep);
            #endregion
        }
        #endregion

        #region TCP的粘包和丢包问题
        /// <summary>
        /// TCP的粘包和丢包问题
        /// 为了解决接收的数据更准确，把数据一次发一次收，不要把两次发的数据搞到一起去。
        /// </summary>
        private void TcpTest()
        {
            #region 服务端代码
            // 创建服务端通讯对象
            Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            // 绑定服务端地址
            server.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9009));
            // 开启监听服务
            server.Listen();
            // 开启监听线程
            Task.Run(async () =>
            {
                Socket c = server.Accept(); // 接收连接 - 服务端接收客户端的请求响应
                List<byte> respBytes = new List<byte>(); // 实例化接收数据集合
                while (true)
                {
                    //await Task.Delay(1000); // 等待1秒钟
                    #region 接收报文头-实际数据长度、
                    // 准确：  两个字节的长度    后面N个数据值
                    //byte[] data = new byte[2]; // 接收数据字节长度 
                    byte[] data = new byte[3]; // 接收数据字节长度 
                    c.Receive(data); // 按照数组长度接收数据 - 接收到的数据在缓冲区内会被移除
                    Console.Write($"{data[0]} -"); // 输出包头的序号
                    Console.WriteLine(Encoding.UTF8.GetString(data)); // 打印接收到的数据
                    #endregion

                    //respBytes.Add(data[0]);
                    //if (respBytes.Count >= 5) 执行委托回调 respBytes.Clear();

                    #region 接收实际数据长度
                    //short len = BitConverter.ToInt16(new byte[] { data[0], data[1] }, 0); // 获取实际数据长度
                    short len = BitConverter.ToInt16(new byte[] { data[1], data[2] }, 0); // 获取实际数据长度
                    data = new byte[len]; // 实例化实际数据对象
                    c.Receive(data);// 接收实际数据
                    Console.WriteLine(Encoding.UTF8.GetString(data)); // 打印实际数据
                    #endregion
                }
            });
            #endregion

            #region 客户端代码
            // 约定-》协议  ModbusTCP  S7   TransactionID zx
            // 实例化客户端通讯对象
            Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            // 连接服务端
            client.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9009));

            for (int i = 0; i < 10; i++) // 循环10次给服务端发送数据
            {
                // 发送端
                //Thread.Sleep(1000); // 如果服务端数据处理比较慢，客户端的数据可以慢一点发送，以避免导致服务端缓冲区的内存溢出
                string msg = "Hello"; //要发送的数据  王下班后 孔 忆 蛋  以心问心 nn8899998766558990 
                byte[] data = Encoding.UTF8.GetBytes(msg); // 发送的数据转byte数组

                List<byte> bytes = new List<byte>(); // 实例化发送数据集合
                //// 包头
                bytes.Add((byte)i);//第几个包 一个字节   255   65535 
                //// 协议名称
                //bytes.AddRange(Encoding.UTF8.GetBytes("zx"));// 两个字节

                short len = (short)data.Length;// 有效数据长度  两个字节(固定的)    0-255    0-65535    自定义协议
                bytes.AddRange(BitConverter.GetBytes(len));// 两字节长度
                bytes.AddRange(data);// 数据有效字节

                client.Send(bytes.ToArray()); // 发送报文
            }
            Console.WriteLine("发送完成");
            #endregion
        }
        #endregion

        #region 解决断线重连问题
        /// <summary>
        /// 解决断线重连问题
        /// </summary>
        private void TcpReconnect()
        {
            // 实例化客户端连接集合
            List<Socket> sockets = new List<Socket>();

            // 实例化服务端通讯对象
            Socket server = new Socket(SocketType.Stream, ProtocolType.Tcp);

            // 相同地址和端口后再连接断开后24内无法再次重新连接
            #region 解决当客户端连接断开24秒内无法再次重新连接问题
            LingerOption lingerOption = new LingerOption(true, 0);
            lingerOption.LingerTime = 0;
            server.LingerState = lingerOption;
            #endregion

            server.Bind(new IPEndPoint(IPAddress.Any, 9090)); // 服务端绑定地址和端口
            server.Listen(1000); // 开启监听服务 允许1000个客户端排队等待连接
            Task.Run(() => // 开启线程接收客户端发送的数据
            {
                while (true) // 循环接收客户端发送的数据
                {
                    var client = server.Accept(); // 服务端接收客户端的请求响应
                    sockets.Add(client); // 添加客户端连接
                    Console.WriteLine(client.RemoteEndPoint); // 输出客户端连接地址

                    Task.Run(() => // 开启线程接收客户端数据
                    {
                        var ccc = client; // 获取客户端连接 - 此处要关注一下，是否会出现引用问题
                        while (true) // 循环获取客户端发送的数据
                        {
                            try
                            {
                                int len = ccc.Receive(new byte[1024]); // 接收客户端发送数据
                                if (len == 0) // 判断没有接收到数据
                                    ccc.Close(); // 关闭连接
                            }
                            catch
                            {
                                ccc.Dispose(); // 释放连接资源
                                sockets.Remove(ccc); // 客户端连接集合中移除链接
                            }
                        }
                    });
                }
            });

            // 这种情况实际是两个客户端连接数
            // 如果服务端允许多连   限制端口
            // 不允许多连    指定端口
            //Socket client = new Socket(SocketType.Stream, ProtocolType.Tcp); // 客户端通讯实例化
            //client.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 6666)); // 客户端绑定地址
            //client.Connect("127.0.0.1", 9090); // 连接服务端
            //client.Close(); // 关闭连接

            ////Thread.Sleep(30_000);

            //client = new Socket(SocketType.Stream, ProtocolType.Tcp);
            //client.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 6666));
            //client.Connect("127.0.0.1", 9090);

            // 客户端通讯实例
            TcpClient tcpClient = new TcpClient();
            // 绑定客户端地址
            tcpClient.Client.Bind(new IPEndPoint(IPAddress.Any, 5500));
            // 连接服务端
            tcpClient.Connect("127.0.0.1", 9090);// 建立服务器连接
            // 关闭连接
            tcpClient.Close();

            Thread.Sleep(1000); // 等待1秒钟
            tcpClient = new TcpClient(); // 客户端通讯实例
            tcpClient.Client.Bind(new IPEndPoint(IPAddress.Any, 5500)); // 客户端地址
            tcpClient.Connect("127.0.0.1", 9090); // 连接服务端
        }
        #endregion

        #region TCPClient代码测试
        /// <summary>
        /// TCPClient代码测试
        /// </summary>
        private void TcpClientTest()
        {
            // 服务端  
            TcpListener tcpListener = new TcpListener(9090); // 实例化服务端网口通讯对象
            tcpListener.Start(); // 开启监听服务
            //Socket socket = tcpListener.AcceptSocket(); // 允许客户端接入的请求，返回的对象是客户端的连接
            Task.Run(() =>  // Task是封装的有些异常外部捕获不到
            {
                while (true)
                {
                    TcpClient client = tcpListener.AcceptTcpClient(); // 允许客户端接入的请求，返回的对象是客户端的连接
                    NetworkStream stream1 = client.GetStream(); // 获取发送数据的流

                    byte[] bytes = new byte[10]; // 实例化接收数据的数组
                    stream1.Read(bytes); // 读取接收到的数据
                    Console.WriteLine(Encoding.UTF8.GetString(bytes)); // 打印接收到的数据

                    byte[] bbb = Encoding.UTF8.GetBytes("回复：已收到"); // 回复
                    stream1.Write(bbb, 0, bbb.Length); // 通过流的方式给客户端回复数据
                }
            });


            // 客户端
            // 为什么构造函数传
            TcpClient tcpClient = new TcpClient(); // 实例化客户端网口通讯对象
            tcpClient.Connect("127.0.0.1", 9090); // 连接服务端

            NetworkStream stream = tcpClient.GetStream(); // 获取流对象用于发送和接收数据
            byte[] datas = Encoding.UTF8.GetBytes("hello"); // 要发送的数据
            stream.Write(datas, 0, datas.Length); // 通过流的方式发送数据


            byte[] aaaa = new byte[20]; // 实例化接收数据对象
            stream.Read(aaaa, 0, aaaa.Length); // 接收数据
            Console.WriteLine(Encoding.UTF8.GetString(aaaa)); // 输出数据 
        }
        #endregion

        #region UdpClient代码测试
        /// <summary>
        /// UdpClient代码测试
        /// </summary>
        private void UdpClientTest()
        {
            // 无连接   不需要Connect
            UdpClient udpClientServer = new UdpClient(9999); // 实例化服务端通讯对象 - 需要指定端口号
            IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, 0); // 指定可以接收的客户端地址以及端口号
            Task.Run(() =>
            {
                while (true)
                {
                    byte[] datas = udpClientServer.Receive(ref endPoint); // 接收数据
                    string str = Encoding.ASCII.GetString(datas);
                    Console.WriteLine(str);
                }
            });


            UdpClient udpClient = new UdpClient(); // 实例化客户端通讯对象
            udpClient.Send(new byte[] { 0x01, 0x02, 0x03 }, "192.168.43.32", 9999); // 向指定的服务端发送数据
        }
        #endregion
    }
}
